Skip to content

Fix off-by-one that corrupts a bitmap in remove_smallest/remove_biggest (#359) - #363

Merged
Kerollmops merged 1 commit into
RoaringBitmap:mainfrom
youdie006:fix/359-interval-remove-boundary
Aug 12, 2026
Merged

Fix off-by-one that corrupts a bitmap in remove_smallest/remove_biggest (#359)#363
Kerollmops merged 1 commit into
RoaringBitmap:mainfrom
youdie006:fix/359-interval-remove-boundary

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

Fixes #359.

Problem

For a run/interval container, IntervalStore::remove_smallest(n) and remove_biggest(n) have an off-by-one at the interval where removal stops when n exactly equals that interval's run length:

  • remove_smallest does start += amount, moving start to end + 1.
  • remove_biggest does end -= amount, moving end to start - 1.

Either way the interval becomes inverted (start > end), and run_len() (end - start + 1) then underflows on the subtraction: a debug-build panic ("attempt to subtract with overflow"), or silent corruption in release. The reporter's example: {0,1,2,4}.remove_smallest(3) should yield {4} but returns a bitmap of length 65537.

Fix

Drop the exactly-consumed interval instead of shrinking it:

  • remove_smallest: if last_interval.run_len() < amount becomes <=, so an interval whose whole run is removed is dropped via remove_to += 1 rather than shrunk.
  • remove_biggest: if last_interval.run_len() >= amount becomes >, so the exactly-consumed interval falls through to the drain(remove_to..) that removes it, instead of shrinking its end.

Test

Added remove_smallest_exact_interval_boundary and remove_biggest_exact_interval_boundary (the reporter's two cases at the IntervalStore level). Red-green verified with cargo test -p roaring: before the fix both assert against inverted intervals ([3, 2] / [4, 3]); after, they drop the whole interval. The full roaring test suite (including the property tests) passes; rustfmt --check and clippy are clean.

When the amount to remove exactly equals the run length of the interval
where removal stops, IntervalStore::remove_smallest shrank that interval
to start = end + 1 and remove_biggest shrank it to end = start - 1,
producing an inverted interval. run_len() then underflows on end - start
(a debug-build panic, silent corruption in release): e.g.
{0,1,2,4}.remove_smallest(3) returned a bitmap of length 65537 instead
of {4}.

Drop the exactly-consumed interval instead of shrinking it: use <= (not
<) in remove_smallest and > (not >=) in remove_biggest so the boundary
interval is removed by the drain. Add boundary tests for both.

Fixes RoaringBitmap#359

@Kerollmops Kerollmops left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @youdie006 👋

Thank you very much to you and @DRMacIver for the report and the bug fix. I'll release this fix as part of the next version release.

Have a nice day 🌵

@Kerollmops
Kerollmops added this pull request to the merge queue Aug 12, 2026
Merged via the queue into RoaringBitmap:main with commit 5e8445b Aug 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remove_smallest / remove_biggest can corrupt a bitmap

2 participants